package com.travel.recommend.strategyPattern;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.travel.common.to.AttractionInfoTo;
import com.travel.recommend.entity.*;
import com.travel.recommend.feign.SightFeignService;
import com.travel.recommend.service.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;

/**
 * 基于用户行为协同过滤
 */
@Service
@Slf4j
public class UserBehaviorRecommendationStrategy implements RecommendationStrategy{
    private final InitialLikeTagService initialLikeTagService;

    private final SightFeignService sightFeignService;

    private final UserTagWeightService userTagWeightService;

    private final TagPopularityService tagPopularityService;

    private final UserAttractionWeightService userAttractionWeightService;

    private final RecommendParameterService recommendParameterService;

    private final SimilarityConfigService similarityConfigService;

    private final UserBehaviorWeightService userBehaviorWeightService;

    public UserBehaviorRecommendationStrategy(InitialLikeTagService initialLikeTagService,
                                     SightFeignService sightFeignService,
                                     UserTagWeightService userTagWeightService,
                                     TagPopularityService tagPopularityService,
                                     UserAttractionWeightService userAttractionWeightService,
                                     RecommendParameterService recommendParameterService,
                                     SimilarityConfigService similarityConfigService,
                                     UserBehaviorWeightService userBehaviorWeightService) {
        this.initialLikeTagService = initialLikeTagService;
        this.sightFeignService = sightFeignService;
        this.userTagWeightService = userTagWeightService;
        this.tagPopularityService = tagPopularityService;
        this.userAttractionWeightService = userAttractionWeightService;
        this.recommendParameterService = recommendParameterService;
        this.similarityConfigService = similarityConfigService;
        this.userBehaviorWeightService = userBehaviorWeightService;
    }


    static double locationWeight; //计算景点相似度中，地理位置权重
    static double tagPopularityWeight; //计算景点相似度中，景点标签权重
    static double likeCollectionCommentWeight; //计算景点相似度中，点赞/收藏/评论权重
    static double clicksWeight; //计算景点相似度中，被点击次数权重

    static int policyDivision; //策略划分值(用户行为数)，<基于标签，>=协同过滤
    static int similarityAttractionNum; //附加几个相似度高的景点
    static int neighboringNum; //基于用户行为，寻找几个最邻近用户

    static double likeWeight; //基于用户行为，点赞权重
    static double collectionWeight; //基于用户行为，收藏权重
    static double commentWeight; //基于用户行为，评论权重
    static double clicksWeight1; //基于用户行为,点击次数系数

    static double init; //热度衰减函数，初始值，上限
    static int m; //热度衰减函数，天数，x轴长度
    static double finish; //热度衰减函数，最低值，下限

    @Override
    public List<Integer> recommend(Integer userId) {
        // 最终要返回的结果：推荐景点的id集合
        List<Integer> recommendAttractionId = new ArrayList<>();

        //1. 获取所有用户对于有行为景点的权重（数据库中获取） 封装为AW便于计算
        List<UserAttractionWeightEntity> list = userAttractionWeightService.list();
        List<AW> AWList = list.stream().collect(Collectors.groupingBy(UserAttractionWeightEntity::getUserId))
                .entrySet().stream()
                .map(entry -> {
                    AW aw = new AW();
                    aw.setUserId(entry.getKey());
                    List<Weight> weightList = entry.getValue().stream()
                            .map(uaw -> {
                                Weight weight = new Weight();
                                weight.setAttractionId(uaw.getAttractionId());
                                weight.setWeight(uaw.getAttractionWeight());
                                return weight;
                            }).collect(Collectors.toList());
                    aw.setWeightList(weightList);
                    return aw;
                }).collect(Collectors.toList());

        System.out.println(AWList);

        //2. 计算得到最邻近的n个用户 有行为的景点 但我们没有行为的景点
        List<Integer> integers = proximityUserBehavior(userId, AWList);
        recommendAttractionId.addAll(integers);

        //3. 附加与用户行为高度相似的景点 不与collect重复
        // 获取该用户有行为的所有景点
        List<UserAttractionWeightEntity> entities = userAttractionWeightService.list(new QueryWrapper<UserAttractionWeightEntity>().eq("user_id", userId));
        List<Integer> ids = entities.stream().map(UserAttractionWeightEntity::getAttractionId).collect(Collectors.toList());
        List<AttractionInfoTo> list1 = sightFeignService.getAttractionsByIds(ids);
        // 该用户有行为的所有景点id
        List<Integer> collect1 = list1.stream().map(AttractionInfoTo::getAttractionId).collect(Collectors.toList());

        // 获取所有景点 过滤掉已经要推荐的（integers）中出现的 过滤掉该用户有行为的（collect1）
        List<AttractionInfoTo> attractionInfoTos = sightFeignService.getAllAttraction();
        List<AttractionInfoTo> list2 = attractionInfoTos.stream()
                .filter(attractionInfoTo -> !collect1.contains(attractionInfoTo.getAttractionId()))
                .filter(attractionInfoTo -> !integers.contains(attractionInfoTo.getAttractionId()))
                .collect(Collectors.toList());


        List<Integer> collect12 = list2.stream().map(AttractionInfoTo::getAttractionId).collect(Collectors.toList());

        System.out.println("用户有行为的景点  "+collect1);
        System.out.println("寻找相似景点的目标集合  "+collect12);

        // 欧式距离度结果集 景点id - <景点id - 欧式距离>
        Map<Integer, Map<Integer,Double>> similar = new HashMap<>();

        list1.forEach(attraction1 ->{
            Map<Integer,Double> map = new HashMap<>();
            list2.forEach(attraction2 -> {
                Double num = cosineSimilarity(attraction1, attraction2);
                map.put(attraction2.getAttractionId(),num);
            });
            similar.put(attraction1.getAttractionId(),map);
        });

        // 1.entrySet方法获取外层Map similar 的键值对集合
        // 2.flatMap方法将内层Map的键值对转换为一个新的Stream
        similar.entrySet().stream().flatMap(entry -> entry.getValue().entrySet().stream())
                .forEach(innerEntry -> System.out.println(innerEntry.getKey() + " : " + innerEntry.getValue()));

        // 取similarityAttractionNum个欧式距离最小的
        List<Integer> minDistances = similar.entrySet().stream().flatMap(entry -> entry.getValue().entrySet().stream())
                .sorted(Map.Entry.comparingByValue())
                .limit(similarityAttractionNum)
                .map(Map.Entry::getKey)
                .collect(Collectors.toList());

        System.out.println("minDistances "+minDistances);

        recommendAttractionId.addAll(minDistances);

        return recommendAttractionId;
    }

    /**
     * 计算得到最邻近的n个用户 有行为的景点 但我们没有行为的景点
     * @param userId
     * @param AWList
     * @return
     */
    public List<Integer> proximityUserBehavior(Integer userId,List<AW> AWList){
        //1. 找到最邻近的n个用户 key：userId value：相似度
        Map<Integer,Double> distances = computeNearestNeighbor(userId, AWList);

        //2. 皮尔逊相关系数范围[-1,1] 且不为NaN
        //proximityUser：与当前用户的皮尔逊系数 降序的 userId
        List<Integer> proximityUser = distances.entrySet().stream().filter(e -> !Double.isNaN(e.getValue()) && Math.abs(e.getValue()) <= 1)
                .sorted(Map.Entry.<Integer, Double>comparingByValue().reversed())
                .map(Map.Entry::getKey)
                .collect(Collectors.toList());
        System.out.println("与当前用户的皮尔逊系数降序的userId"+proximityUser);

        //3. 取前neighboringNum个，即最相似的几个用户
        List<Integer> integers = proximityUser.subList(0, Math.min(proximityUser.size(), 3));

        //4. 遍历获取这几个用户有行为的景点 但我们没有的景点
        // 当前用户的AW
        AW u1 = AWList.stream().filter(aw -> aw.getUserId() == userId)
                .findFirst()
                .orElse(null);

        // 邻居用户的AW
        List<AW> proximityUserAW = AWList.stream().filter(aw -> integers.contains(aw.getUserId())).collect(Collectors.toList());
        // 邻近用户有行为但当前用户没有行为的景点（权重降序）
        List<Integer> collect = proximityUserAW.stream().filter(aw -> aw.getUserId() != userId)  // 过滤出除当前用户外的所有用户
                .flatMap(aw -> aw.getWeightList().stream())  // 将所有用户的weightList合并成一个流
                .filter(w -> u1.getWeightList().stream().mapToInt(Weight::getAttractionId).noneMatch(attractionId -> attractionId == w.getAttractionId()))  // 过滤掉与当前用户相同的attractionId
                .distinct()  // 去重
                .sorted(Comparator.comparingDouble(Weight::getWeight).reversed())
                .map(Weight::getAttractionId)  // 提取attractionId
                .collect(Collectors.toList());

        System.out.println("邻近用户有行为但当前用户没有行为的景点（权重降序）"+collect);




        return collect;
    }

    /**
     * 根据两个用户对有行为景点的权重
     * 计算两个用户的皮尔逊相关系数
     * @param rating1：当前用户
     * @param rating2：其它用户
     * @return
     */
    private double pearsonDis(List<Weight> rating1, List<Weight> rating2){
        HashMap<Integer, Double> rating1Map = rating1.stream()
                .collect(Collectors.toMap(Weight::getAttractionId, Weight::getWeight, (oldValue, newValue) -> newValue, HashMap::new));

        HashMap<Integer, Double> rating2Map = rating2.stream()
                .collect(Collectors.toMap(Weight::getAttractionId, Weight::getWeight, (oldValue, newValue) -> newValue, HashMap::new));

        //1. 筛选出rating1中rating2中公共的景点id
        Set<Integer> commonAttractionIds = rating1Map.keySet().stream()
                .filter(rating2Map::containsKey)
                .collect(Collectors.toSet());
        System.out.println("===公共景点id===");
        commonAttractionIds.forEach(System.out::println);

        int n = commonAttractionIds.size();
        Double Ex = 0.0;
        Double Ey = 0.0;
        Double Exy = 0.0;
        Double Ex2 = 0.0;
        Double Ey2 = 0.0;

        // 取共同的景点id
        for (Integer attractionId : commonAttractionIds) {
            Double x = rating1Map.get(attractionId); //获取对应权重
            Double y = rating2Map.get(attractionId); //获取对应权重
            Ex += x; //累加权重
            Ey += y; //累加权重
            Exy += x * y; //累加权重积的和
            Ex2 += Math.pow(x, 2); //累加权重的平方和
            Ey2 += Math.pow(y, 2); //累加权重的平方和
        }

        //分子
        double numerator = Exy - Ex * Ey / n;
        //分母
        double denominator = Math.sqrt((Ex2 - Math.pow(Ex, 2) / n) * (Ey2 - Math.pow(Ey, 2) / n));

        log.info("n===" + n);
        log.info("Ex===" + Ex);
        log.info("Ey===" + Ey);
        log.info("Exy===" + Exy);
        log.info("Ex2===" + Ex2);
        log.info("Ey2===" + Ey2);
        log.info("numerator===" + numerator);
        log.info("denominator===" + denominator);
        log.info("===" + numerator / denominator);

        if (denominator == 0) return 0.0;

        return numerator / denominator;
    }

    /**
     * 计算两个景点的欧式距离
     *
     * @return
     */
    public Double cosineSimilarity(AttractionInfoTo to1,AttractionInfoTo to2){
        Double score1 = to1.getScore();
        Integer clickNum1 = to1.getClickNum();
        Integer likeNum1 = to1.getLikeNum();
        Integer collectionNum1 = to1.getCollectionNum();
        Integer commentNum1 = to1.getCommentNum();
        TagPopularityEntity tag1 = tagPopularityService.getOne(new QueryWrapper<TagPopularityEntity>().eq("tag", to1.getTag()));
        Double popularity1 = tag1.getPopularity();
        String location1 = to1.getLocation();

        Double score2 = to2.getScore();
        Integer clickNum2 = to2.getClickNum();
        Integer likeNum2 = to2.getLikeNum();
        Integer collectionNum2 = to2.getCollectionNum();
        Integer commentNum2 = to2.getCommentNum();
        TagPopularityEntity tag2 = tagPopularityService.getOne(new QueryWrapper<TagPopularityEntity>().eq("tag", to2.getTag()));
        Double popularity2 = tag2.getPopularity();
        String location2 = to2.getLocation();

        // 加权处理
        double weightedClickNum1 = clickNum1 * clicksWeight;
        double weightedLikeNum1 = likeNum1 * likeCollectionCommentWeight;
        double weightedCollectionNum1 = collectionNum1 * likeCollectionCommentWeight;
        double weightedCommentNum1 = commentNum1 * likeCollectionCommentWeight;
        double weightedPopularity1 = popularity1 * tagPopularityWeight;

        double weightedClickNum2 = clickNum2 * clicksWeight;
        double weightedLikeNum2 = likeNum2 * likeCollectionCommentWeight;
        double weightedCollectionNum2 = collectionNum2 * likeCollectionCommentWeight;
        double weightedCommentNum2 = commentNum2 * likeCollectionCommentWeight;
        double weightedPopularity2 = popularity2 * tagPopularityWeight;

        // 计算欧式距离
        double distance = Math.sqrt(Math.pow(weightedClickNum1 - weightedClickNum2, 2) + Math.pow(weightedLikeNum1 - weightedLikeNum2, 2)
                + Math.pow(weightedCollectionNum1 - weightedCollectionNum2, 2) + Math.pow(weightedCommentNum1 - weightedCommentNum2, 2)
                + Math.pow(weightedPopularity1 - weightedPopularity2, 2) + Math.pow(score1 - score2, 2));

        // 如果地区相同 距离除以10
        if(location2.equals(location1)){
            distance /= 10;
        }

        log.info(to1.getName() + " " + to2.getName() + " 距离 " + distance);

        return distance;
    }

    /**
     * 计算其他用户和该用户的距离并排序
     */
    private Map<Integer, Double> computeNearestNeighbor(Integer userId, List<AW> AWList) {
        Map<Integer,Double> distances = new TreeMap<>();

        // 当前用户的AW
        AW u1 = AWList.stream().filter(aw -> aw.getUserId() == userId)
                .findFirst()
                .orElse(null);

        // 其它用户的AW
        AWList.stream().filter(aw -> aw.getUserId() != userId).forEach(u2 -> {
            // 计算当前用户与其它用户的距离并排序
            double distance = pearsonDis(u1.getWeightList(),u2.getWeightList());
            // 记录与其它用户的距离
            distances.put(u2.getUserId(),distance);
            System.out.println("与用户 "+u2.getUserId() +" 的皮尔逊系数为 " + distance);
        });

        return distances;
    }

    @Override
    public void initAlgorithmConfig() {
        RecommendParameterEntity recommendParameterEntity = recommendParameterService.getNowRecommendParameter();
        SimilarityConfigEntity similarityConfigEntity = similarityConfigService.getNowSimilarityConfig();
        UserBehaviorWeightEntity userBehaviorWeightEntity = userBehaviorWeightService.getNowUserBehaviorWeight();

        locationWeight = similarityConfigEntity.getLocationWeight(); //计算景点相似度中，地理位置权重
        tagPopularityWeight = similarityConfigEntity.getTagPopularityWeight(); //计算景点相似度中，景点标签权重
        likeCollectionCommentWeight = similarityConfigEntity.getLikeCollectionCommentWeight(); //计算景点相似度中，点赞/收藏/评论权重
        clicksWeight = similarityConfigEntity.getClicksWeight(); //计算景点相似度中，被点击次数权重

        similarityAttractionNum = recommendParameterEntity.getSimilarityAttractionNum(); //基于标签，附加几个相似度高的景点
        neighboringNum = recommendParameterEntity.getNeighboringNum(); //基于用户行为，寻找几个最邻近用户
        policyDivision = recommendParameterEntity.getPolicyDivision(); //策略划分值(用户行为数)，<基于标签，>=协同过滤

        likeWeight = userBehaviorWeightEntity.getLikeWeight(); //基于用户行为，点赞权重
        collectionWeight = userBehaviorWeightEntity.getCollectionWeight(); //基于用户行为，收藏权重
        commentWeight = userBehaviorWeightEntity.getCommentWeight(); //基于用户行为，评论权重
        clicksWeight1 = userBehaviorWeightEntity.getClicksWeight(); //基于用户行为,点击次数系数
    }
}
